package com.wuhdev.swaggergenerator.utils;

import com.google.common.base.Strings;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiShortNamesCache;
import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.model.*;
import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.io.StringReader;
import java.util.*;
import java.util.stream.Collectors;

public class Generator {

    private static final String MAPPING_VALUE = "value";
    private static final String MAPPING_METHOD = "method";
    private static final String REQUEST_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.RequestMapping";
    private static final String POST_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.PostMapping";
    private static final String PUB_ACTION_ANNOTATION = "com.mysoft.framework.rpc.annotation.PubAction";
    private static final String GET_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.GetMapping";
    private static final String DELETE_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.DeleteMapping";
    private static final String PATCH_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.PatchMapping";
    private static final String PUT_MAPPING_ANNOTATION = "org.springframework.web.bind.annotation.PutMapping";
    private static final String REQUEST_PARAM_TEXT = "org.springframework.web.bind.annotation.RequestParam";
    private static final String REQUEST_HEADER_TEXT = "org.springframework.web.bind.annotation.RequestHeader";
    private static final String PATH_VARIABLE_TEXT = "org.springframework.web.bind.annotation.PathVariable";
    private static final String REQUEST_BODY_TEXT = "org.springframework.web.bind.annotation.RequestBody";
    private static final List<String> CONTROLLER_ANNOTATION = Arrays.asList("org.springframework.stereotype.Controller", "org.springframework.web.bind" +
            ".annotation.RestController", "com.mysoft.framework.rpc.annotation.PubService");

    private final Project project;
    private final PsiFile psiFile;
    private final PsiClass psiClass;
    private final PsiElementFactory elementFactory;
    private final String selectionText;
    private final JavaProjectBuilder javaProjectBuilder;

    public Generator(Project project, PsiFile psiFile, PsiClass psiClass, String selectionText) {
        this.project = project;
        this.psiFile = psiFile;
        this.psiClass = psiClass;
        this.selectionText = selectionText;
        this.elementFactory = JavaPsiFacade.getElementFactory(project);
        this.javaProjectBuilder = new JavaProjectBuilder();
        this.javaProjectBuilder.setEncoding("UTF-8");
        this.javaProjectBuilder.addSource(new StringReader(psiFile.getText()));
    }

    private JavaClass findJavaClass(PsiClass psiClass) {
        return this.javaProjectBuilder.getClasses()
                                      .stream()
                                      .filter(x -> Objects.equals(x.getFullyQualifiedName(), psiClass.getQualifiedName()))
                                      .findFirst()
                                      .orElse(null);
    }

    private String findDescription(Collection<DocletTag> tags) {
        Optional<DocletTag> docletTag = tags.stream()
                                            .filter(x -> Objects.equals(x.getName(), "apiNote"))
                                            .findFirst();
        return docletTag.map(tag -> tag.getValue()
                                       .trim())
                        .orElse("");
    }

    private String findReturnDescription(Collection<DocletTag> tags) {
        Optional<DocletTag> docletTag = tags.stream()
                                            .filter(x -> Objects.equals(x.getName(), "return"))
                                            .findFirst();
        return docletTag.map(tag -> tag.getValue()
                                       .trim())
                        .orElse("");
    }

    private Map<String, String> findParamDescription(Collection<DocletTag> tags) {
        return tags.stream()
                   .filter(x -> Objects.equals(x.getName(), "param"))
                   .map(x -> {
                       List<String> splitStringList = new ArrayList<>(Arrays.stream(x.getValue()
                                                                                     .split(" "))
                                                                            .filter(s -> !Strings.isNullOrEmpty(s))
                                                                            .toList());
                       String name = splitStringList.get(0);
                       splitStringList.remove(0);
                       String comment = String.join(" ", splitStringList);
                       return Map.entry(name, comment);
                   })
                   .filter(x -> !Strings.isNullOrEmpty(x.getValue()))
                   .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    public void doGenerate() {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            boolean selection = StringUtils.isNotEmpty(selectionText);
            // 遍历当前对象的所有属性
            boolean isController = this.isController(psiClass);
            if (selection) {
                this.generateSelection(psiClass, selectionText, isController);
                return;
            }

            // 获取类注释
            this.generateClassAnnotation(psiClass, isController);
            JavaClass javaClass = Objects.requireNonNull(this.findJavaClass(psiClass));
            if (isController) {
                // 类方法列表
                PsiMethod[] methods = psiClass.getMethods();
                for (PsiMethod psiMethod : methods) {
                    this.generateMethodAnnotation(psiMethod, javaClass);
                }
            } else {
                PsiClass[] innerClasses = psiClass.getInnerClasses();
                for (PsiClass innerClass : innerClasses) {
                    this.generateClassAnnotation(innerClass, false);
                    // 类属性列表
                    PsiField[] field = innerClass.getAllFields();
                    for (PsiField psiField : field) {
                        this.generateFieldAnnotation(psiField, javaClass);
                    }
                }

                // 类属性列表
                PsiField[] field = psiClass.getAllFields();
                for (PsiField psiField : field) {
                    this.generateFieldAnnotation(psiField, javaClass);
                }
            }
        });
    }

    /**
     * 写入到文件
     *
     * @param name                 注解名
     * @param qualifiedName        注解全包名
     * @param annotationText       生成注解文本
     * @param psiModifierListOwner 当前写入对象
     */
    private void doWrite(String name, String qualifiedName, String annotationText, PsiModifierListOwner psiModifierListOwner) {
        PsiAnnotation psiAnnotationDeclare = elementFactory.createAnnotationFromText(annotationText, psiModifierListOwner);
        final PsiNameValuePair[] attributes = psiAnnotationDeclare.getParameterList()
                                                                  .getAttributes();
        PsiAnnotation existAnnotation = Objects.requireNonNull(psiModifierListOwner.getModifierList())
                                               .findAnnotation(qualifiedName);
        if (existAnnotation != null) {
            existAnnotation.delete();
        }
        addImport(elementFactory, psiFile, name, qualifiedName);
        PsiAnnotation psiAnnotation = psiModifierListOwner.getModifierList()
                                                          .addAnnotation(name);
        for (PsiNameValuePair pair : attributes) {
            psiAnnotation.setDeclaredAttributeValue(pair.getName(), pair.getValue());
        }
    }

    /**
     * 写入到文件
     *
     * @param name                 注解名
     * @param qualifiedName        注解全包名
     * @param annotationTexts       生成注解文本列表
     * @param psiModifierListOwner 当前写入对象
     */
    private void doWrite(String name, String qualifiedName, Collection<String> annotationTexts, PsiModifierListOwner psiModifierListOwner) {
        PsiModifierList psiModifierList = Objects.requireNonNull(psiModifierListOwner.getModifierList());
        PsiAnnotation existAnnotation;
        while ((existAnnotation = psiModifierList.findAnnotation(qualifiedName)) != null) {
            existAnnotation.delete();
        }

        addImport(elementFactory, psiFile, name, qualifiedName);
        for (String annotationText : annotationTexts) {
            PsiAnnotation psiAnnotationDeclare = elementFactory.createAnnotationFromText(annotationText, psiModifierListOwner);
            final PsiNameValuePair[] attributes = psiAnnotationDeclare.getParameterList()
                                                                      .getAttributes();
            PsiAnnotation psiAnnotation = psiModifierList.addAnnotation(name);
            for (PsiNameValuePair pair : attributes) {
                psiAnnotation.setDeclaredAttributeValue(pair.getName(), pair.getValue());
            }
        }
    }

    /**
     * 写入到文件
     *
     * @param qualifiedName        注解全包名
     * @param annotationText       生成注解文本
     * @param psiModifierListOwner 当前写入对象
     */
    private void doWrite(String qualifiedName, String annotationText, PsiModifierListOwner psiModifierListOwner) {
        PsiModifierList psiModifierList = Objects.requireNonNull(psiModifierListOwner.getModifierList());
        PsiAnnotation existAnnotation;
        while ((existAnnotation = psiModifierList.findAnnotation(qualifiedName)) != null) {
            existAnnotation.delete();
        }
        PsiAnnotation psiAnnotationDeclare = elementFactory.createAnnotationFromText(annotationText, psiModifierListOwner);
        final PsiNameValuePair[] attributes = psiAnnotationDeclare.getParameterList()
                                                                  .getAttributes();
        PsiAnnotation psiAnnotation = psiModifierListOwner.getModifierList()
                                                          .addAnnotation(qualifiedName);
        for (PsiNameValuePair pair : attributes) {
            psiAnnotation.setDeclaredAttributeValue(pair.getName(), pair.getValue());
        }
    }

    private void deleteAnnotation(String qualifiedName, PsiModifierListOwner psiModifierListOwner) {
        PsiModifierList psiModifierList = Objects.requireNonNull(psiModifierListOwner.getModifierList());
        PsiAnnotation existAnnotation;
        while ((existAnnotation = psiModifierList.findAnnotation(qualifiedName)) != null) {
            existAnnotation.delete();
        }
    }

    /**
     * 类是否为 controller
     *
     * @param psiClass 类元素
     */
    private void generateSelection(PsiClass psiClass, String selectionText, boolean isController) {
        if (Objects.equals(selectionText, psiClass.getName())) {
            this.generateClassAnnotation(psiClass, isController);
        }
        PsiMethod[] methods = psiClass.getMethods();
        JavaClass javaClass = Objects.requireNonNull(this.findJavaClass(psiClass));
        for (PsiMethod psiMethod : methods) {
            if (Objects.equals(selectionText, psiMethod.getName())) {
                this.generateMethodAnnotation(psiMethod, javaClass);
                return;
            }
        }
        PsiField[] field = psiClass.getAllFields();
        for (PsiField psiField : field) {
            if (Objects.equals(selectionText, psiField.getNameIdentifier()
                                                      .getText())) {
                this.generateFieldAnnotation(psiField, javaClass);
                return;
            }
        }
    }

    /**
     * 类是否为controller
     *
     * @param psiClass 类元素
     * @return boolean
     */
    private boolean isController(PsiClass psiClass) {
        PsiAnnotation[] psiAnnotations = Objects.requireNonNull(psiClass.getModifierList())
                                                .getAnnotations();
        for (PsiAnnotation psiAnnotation : psiAnnotations) {
            if (CONTROLLER_ANNOTATION.contains(psiAnnotation.getQualifiedName())) {
                // controller
                return true;
            }
        }

        if (Objects.requireNonNull(psiClass.getName())
                   .endsWith("Controller")) {
            // 根据命名约定推断
            return true;
        }

        return false;
    }

    private String trimComment(String comment) {
        if (comment == null) {
            return comment;
        }

        comment = comment.trim();
        if (comment.startsWith("description")) {
            comment = comment.substring("description".length()).trim();
        }

        return comment;
    }

    /**
     * 是否泛型类型
     * @param psiType 类型
     * @return boolean
     */
    private boolean isGenericType(PsiType psiType) {
        if (psiType == null) {
            return false;
        }

        if (!(psiType instanceof PsiClassReferenceType)) {
            return false;
        }

        return ((PsiClassReferenceType)psiType).getParameterCount() > 0;
    }

    /**
     * 获取RequestMapping注解属性
     *
     * @param psiAnnotations 注解元素数组
     * @param attributeName  属性名
     * @return String 属性值
     */
    private String getMappingAttribute(PsiAnnotation[] psiAnnotations, String attributeName) {
        for (PsiAnnotation psiAnnotation : psiAnnotations) {
            switch (Objects.requireNonNull(psiAnnotation.getQualifiedName())) {
                case REQUEST_MAPPING_ANNOTATION:
                case PUB_ACTION_ANNOTATION:
                    String attribute = getAttribute(psiAnnotation, attributeName, "");
                    if (Objects.equals("\"\"", attribute)) {
                        return "";
                    }
                    return attribute;
                case POST_MAPPING_ANNOTATION:
                    return "POST";
                case GET_MAPPING_ANNOTATION:
                    return "GET";
                case DELETE_MAPPING_ANNOTATION:
                    return "DELETE";
                case PATCH_MAPPING_ANNOTATION:
                    return "PATCH";
                case PUT_MAPPING_ANNOTATION:
                    return "PUT";
                default:
                    break;
            }
        }
        return "";
    }

    /**
     * 获取注解属性
     *
     * @param psiAnnotation 注解全路径
     * @param attributeName 注解属性名
     * @return 属性值
     */
    private String getAttribute(PsiAnnotation psiAnnotation, String attributeName, String comment) {
        if (Objects.isNull(psiAnnotation)) {
            return "\"" + comment + "\"";
        }
        PsiAnnotationMemberValue psiAnnotationMemberValue = psiAnnotation.findDeclaredAttributeValue(attributeName);
        if (Objects.isNull(psiAnnotationMemberValue)) {
            return "\"" + comment + "\"";
        }
        return psiAnnotationMemberValue.getText();
    }

    /**
     * 生成类注解
     *
     * @param psiClass     类元素
     * @param isController 是否为controller
     */
    private void generateClassAnnotation(PsiClass psiClass, boolean isController) {
        JavaClass javaClass = this.findJavaClass(psiClass);
        String classComment = javaClass.getComment();
        if (Strings.isNullOrEmpty(classComment)) {
            return;
        }

        String annotationFromText;
        String annotation;
        String qualifiedName;
        String classDescription = this.findDescription(javaClass.getTags());
        if (isController) {
            annotation = "Tag";
            qualifiedName = "io.swagger.v3.oas.annotations.tags.Tag";
        } else {
            annotation = "Schema";
            qualifiedName = "io.swagger.v3.oas.annotations.media.Schema";
        }

        if (Strings.isNullOrEmpty(classDescription)) {
            classDescription = classComment;
        }
        annotationFromText = String.format("@%s(name = \"%s\", description = \"%s\")", annotation, classComment, classDescription);
        this.doWrite(annotation, qualifiedName, annotationFromText, psiClass);
    }

    /**
     * 生成方法注解
     *
     * @param psiMethod 类方法元素
     */
    private void generateMethodAnnotation(PsiMethod psiMethod, JavaClass javaClass) {
        if (psiMethod.isConstructor()) {
            // 构造函数不处理
            return;
        }

        List<JavaType> parameterTypes = Arrays.stream(psiMethod.getParameterList()
                                                               .getParameters())
                                              .map(p -> (JavaType) this.javaProjectBuilder.getClassByName(((PsiClassReferenceType) p.getType()).rawType()
                                                                                                                                               .getCanonicalText()))
                                              .toList();
        JavaMethod javaMethod = javaClass.getMethodBySignature(psiMethod.getName(), parameterTypes);
        if (javaMethod == null) {
            return;
        }

        String methodComment = this.trimComment(javaMethod.getComment());
        if (Strings.isNullOrEmpty(methodComment)) {
            return;
        }

        String methodDescription = this.findDescription(javaMethod.getTags());
        if (Strings.isNullOrEmpty(methodDescription)) {
            methodDescription = methodComment;
        }

        String apiOperationAnnotationText = String.format("@Operation(summary = \"%s\", description = \"%s\")", methodComment, methodDescription);
        ;
        this.doWrite("Operation", "io.swagger.v3.oas.annotations.Operation", apiOperationAnnotationText, psiMethod);
        addImport(elementFactory, psiFile, "Operation", "io.swagger.v3.oas.annotations.Operation");

        Map<String, String> paramDescriptionMap = this.findParamDescription(javaMethod.getTags());
        PsiParameter[] psiParameters = psiMethod.getParameterList()
                                                .getParameters();
        List<String> parameterList = new ArrayList<>(psiParameters.length);
        for (PsiParameter psiParameter : psiParameters) {
            String paramType = "ParameterIn.QUERY";
            for (PsiAnnotation psiAnnotation : Objects.requireNonNull(psiParameter.getModifierList())
                                                      .getAnnotations()) {
                if (StringUtils.isEmpty(psiAnnotation.getQualifiedName())) {
                    break;
                }
                switch (psiAnnotation.getQualifiedName()) {
                    case REQUEST_HEADER_TEXT -> paramType = "ParameterIn.HEADER";
                    case REQUEST_PARAM_TEXT, REQUEST_BODY_TEXT -> paramType = "ParameterIn.QUERY";
                    case PATH_VARIABLE_TEXT -> paramType = "ParameterIn.PATH";
                    default -> {
                    }
                }
            }

            String paramName = psiParameter.getName();
            String paramDescription = paramDescriptionMap.getOrDefault(paramName, paramName);
//            String dataType = CommentUtils.getDataType(psiParameter.getType());
//            String schema = String.format("@Schema(implementation = %s.class)", dataType);
            String parameterText = String.format("@Parameter(name = \"%s\", description = \"%s\", in = %s)", paramName, paramDescription,
                    paramType);
            parameterList.add(parameterText);
        }

        if (parameterList.size() > 0) {
            this.deleteAnnotation("io.swagger.v3.oas.annotations.Parameters", psiMethod);
            this.doWrite("Parameter", "io.swagger.v3.oas.annotations.Parameter", parameterList, psiMethod);
            addImport(elementFactory, psiFile, "ParameterIn", "io.swagger.v3.oas.annotations.enums.ParameterIn");
            addImport(elementFactory, psiFile, "Schema", "io.swagger.v3.oas.annotations.media.Schema");
        } else {
            this.deleteAnnotation("io.swagger.v3.oas.annotations.Parameters", psiMethod);
            this.deleteAnnotation("io.swagger.v3.oas.annotations.Parameter", psiMethod);
        }

        // Describing Request Body
//        if (!Strings.isNullOrEmpty(requestBodyParamDescription)) {
//            String requestBodyAnnotationText = String.format("@io.swagger.v3.oas.annotations.parameters.RequestBody(description = \"%s\")", requestBodyParamDescription);
//            this.doWrite("io.swagger.v3.oas.annotations.parameters.RequestBody", requestBodyAnnotationText, psiMethod);
//        }

        String returnDescription = this.findReturnDescription(javaMethod.getTags());
        if (Strings.isNullOrEmpty(returnDescription)) {
            returnDescription = methodComment;
        }

        String returnAnnotationText = String.format("@ApiResponse(responseCode = \"200\", description = \"%s\", useReturnTypeSchema = true)", returnDescription);
        this.doWrite("ApiResponse", "io.swagger.v3.oas.annotations.responses.ApiResponse", returnAnnotationText, psiMethod);
        this.deleteAnnotation("io.swagger.v3.oas.annotations.responses.ApiResponses", psiMethod);
        addImport(elementFactory, psiFile, "Content", "io.swagger.v3.oas.annotations.media.Content");
    }

    /**
     * 生成属性注解
     *
     * @param psiField 类属性元素
     */
    private void generateFieldAnnotation(PsiField psiField, JavaClass javaClass) {
        JavaField javaField = javaClass.getFieldByName(psiField.getName());
        if (javaField == null) {
            return;
        }

        String fieldComment = this.trimComment(javaField.getComment());
        if (Strings.isNullOrEmpty(fieldComment)) {
            return;
        }

        PsiAnnotation psiAnnotation = Objects.requireNonNull(psiField.getModifierList())
                                             .findAnnotation("com.fasterxml.jackson.annotation.JsonProperty");
        if (psiAnnotation != null) {
            // 如果 JsonProperty 指定了字段名且和类字段名不一致，增加 name
            PsiAnnotationMemberValue psiAnnotationMemberValue = psiAnnotation.findDeclaredAttributeValue("value");
            if (psiAnnotationMemberValue != null && !Objects.equals(psiAnnotationMemberValue.getText(), "\"" + psiField.getName() + "\"")) {
                String propertyAnnotationText = String.format("@Schema(name = %s, description = \"%s\")", psiAnnotationMemberValue.getText(), fieldComment);
                this.doWrite("Schema", "io.swagger.v3.oas.annotations.media.Schema", propertyAnnotationText, psiField);
                return;
            }
        }

        String propertyAnnotationText = String.format("@Schema(description = \"%s\")", fieldComment);
        this.doWrite("Schema", "io.swagger.v3.oas.annotations.media.Schema", propertyAnnotationText, psiField);
    }

    /**
     * 导入类依赖
     *
     * @param elementFactory 元素Factory
     * @param file           当前文件对象
     * @param className      类名
     * @param qualifiedName
     */
    private void addImport(PsiElementFactory elementFactory, PsiFile file, String className, String qualifiedName) {
        if (!(file instanceof PsiJavaFile javaFile)) {
            return;
        }
        // 获取所有导入的包
        final PsiImportList importList = javaFile.getImportList();
        if (importList == null) {
            return;
        }
        PsiClass[] psiClasses = PsiShortNamesCache.getInstance(project)
                                                  .getClassesByName(className, GlobalSearchScope.allScope(project));
        PsiClass waiteImportClass;
        if (psiClasses.length == 0) {
            return;
        } else if (psiClasses.length > 1) {
            waiteImportClass = Arrays.stream(psiClasses)
                                     .filter(x -> Objects.equals(x.getQualifiedName(), qualifiedName))
                                     .findFirst()
                                     .orElse(null);
            if (waiteImportClass == null) {
                return;
            }
        } else {
            waiteImportClass = psiClasses[0];
        }

        for (PsiImportStatementBase is : importList.getAllImportStatements()) {
            String impQualifiedName = Objects.requireNonNull(is.getImportReference())
                                             .getQualifiedName();
            if (Objects.equals(waiteImportClass.getQualifiedName(), impQualifiedName)) {
                // 已经导入
                return;
            }
        }
        importList.add(elementFactory.createImportStatement(waiteImportClass));
    }
}
